---
title: Authentication and authorization
summary: Identity establishment, authorization enforcement, per-operation access requirements, and credential handling.
status: draft
owner: Not determined from source
last_reviewed: 2026-09-22
---

# Authentication and authorization

`clsADUserServices` declares Windows authentication in `Web.config` but ships no `<authorization>` element, and the release pipeline enables IIS anonymous authentication for both deployment variants — so the effective barrier in front of the service is not the Windows identity but a single application-layer, programmatic, machine-level (network-origin) IP allow-list. That allow-list gates 8 of the 19 published operations; the remaining 11, including several that disclose Active Directory attribute and group data, carry no application-layer check at all.

## Authentication

| Aspect | Finding |
| --- | --- |
| Mechanism | Windows/NTLM declared via `<authentication mode="Windows"/>`, but IIS anonymous authentication is enabled for both deployment variants by the release pipeline |
| Established by | `Web.config` declaration; effective enforcement owned by IIS site configuration and the external deploy template |
| `<authentication>` element | `mode="Windows"` |
| Identity type | Not determined from source — no `User.Identity`, `WindowsIdentity`, or `IsInRole` usage exists anywhere in the codebehind |
| Session state | Not used — every `<WebMethod()>` is declared bare, so `EnableSession` defaults to `False` on all 19 operations |
| Anonymous access | Allowed — `IISAnonymousAuthInternal: true` and `IISAnonymousAuthExternal: true` in `pipelines\release.yml` for both variants |

`Web.config` declares `<authentication mode="Windows"/>` with no `<authorization>`, `<identity impersonate>`, or `<location>` element anywhere in the file. The release pipeline enables anonymous authentication for both the internal and external deployment variants, and never supplies `IISWindowsAuthInternal`/`IISWindowsAuthExternal` to the external deploy template, so the effective IIS Windows-authentication state is not determinable from this repository.** In combination, no caller is required to present a Windows identity to reach any operation; the only application-layer control is the IP allow-list documented below. `AddUser.inPassword`, `ChangeUserPassword.inNewPassword`, and `ChangeUserPasswordOrGetException.inNewPassword` accept a plaintext account password as a SOAP string parameter; these travel in the request body and their protection depends entirely on whatever transport binding fronts the deployed endpoint, which is not evidenced in this repository.**

## Authorization enforcement

| Mechanism | Scope | Declared in | Effect |
| --- | --- | --- | --- |
| Programmatic check (`CheckAccess().HasAccess`) | Operation | `src\ADUserServices\clsADUserServices.asmx.vb` | Grants access to a request that is local to the server, or whose observed source address exactly matches a configured allow-list entry; applied to 8 mutating/password operations |
| None in code | 11 read operations | — | Every reachable request invokes the operation; access relies entirely on IIS anonymous authentication being disabled by the external deploy template, or on network placement, neither of which is confirmed by this repository |

This is a network-origin (IP) allow-list, not a role check, a claim check, or a SAIF role-model integration. It carries no notion of which user or calling application is acting, so no operation can be constrained to a subset of accounts, groups, or attributes.

## Access requirements by operation

| Operation | Requirement | Enforced by | On failure |
| --- | --- | --- | --- |
| [`HelloWorld`](contracts/clsaduserservices.md#helloworld) | None in code | — | Call proceeds; cannot fail |
| [`AddUser`](contracts/clsaduserservices.md#adduser) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:105` | `AccessDenied` sentinel |
| [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:202` | `AccessDenied` sentinel |
| [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:234` | `AccessDenied` sentinel |
| [`DeleteUser`](contracts/clsaduserservices.md#deleteuser) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:273` | `AccessDenied` sentinel |
| [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:302` | `AccessDenied` sentinel |
| [`DoesUserExist`](contracts/clsaduserservices.md#doesuserexist) | None in code | — | Call proceeds |
| [`DoesUserHaveResource`](contracts/clsaduserservices.md#doesuserhaveresource) | None in code | — | Call proceeds |
| [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:395` | `AccessDenied` sentinel |
| [`ChangeUserPassword`](contracts/clsaduserservices.md#changeuserpassword) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:474` | `AccessDenied` sentinel |
| [`ChangeUserPasswordOrGetException`](contracts/clsaduserservices.md#changeuserpasswordorgetexception) | IP allow-list or local request | `CheckAccess().HasAccess`, `clsADUserServices.asmx.vb:495` | `AccessDenied` sentinel |
| [`GetUserEmail`](contracts/clsaduserservices.md#getuseremail) | None in code | — | Call proceeds; discloses `mail` |
| [`GetUserGroups`](contracts/clsaduserservices.md#getusergroups) | None in code | — | Call proceeds; discloses transitive group membership |
| [`GetUserResources`](contracts/clsaduserservices.md#getuserresources) | None in code | — | Call proceeds; discloses filtered resource-group membership |
| [`GetUserFAX`](contracts/clsaduserservices.md#getuserfax) | None in code | — | Call proceeds; discloses `facsimileTelephoneNumber` |
| [`GetUserLastLogon`](contracts/clsaduserservices.md#getuserlastlogon) | None in code | — | Call proceeds; discloses last-logon timestamp |
| [`GetUserPrinter`](contracts/clsaduserservices.md#getuserprinter) | None in code | — | Call proceeds; discloses `extensionAttribute2` |
| [`IsUserInGroup`](contracts/clsaduserservices.md#isuseringroup) | None in code | — | Call proceeds; discloses direct group membership |
| [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) | None in code — it is itself the access-control primitive | — | Call proceeds; discloses the caller's observed remote address and the allow-list decision |

## Service account credentials

| Credential | Purpose | Supplied to | Retrieved from | Notes |
| --- | --- | --- | --- | --- |
| IIS application-pool identity | Authenticates the service process to Active Directory for every directory read and write | Every mutating operation, executed as the app-pool identity | Azure DevOps variable group `Architecture.ServiceAccounts.IIS` (`IISAppPoolUserNonProd`/`IISAppPoolPasswordNonProd`, `IISAppPoolUserProd`/`IISAppPoolPasswordProd`) | The same account variables are used for non-prod and prod ([SEC-07](tech-debt.md#high), MEDIUM for identity reuse; the externalization mechanism itself is correct). No `PrincipalContext` in source is constructed with explicit credentials, so every AD action is attributed to this one shared account with no requester correlation |

## Secrets handling

| Secret | Location | Retrieved by | Consumed by | Notes |
| --- | --- | --- | --- | --- |
| Default Active Directory account password | Embedded literal in source (`src\ADUserServices\clsADUserServices.asmx.vb`, `AddUser`, the `If inPassword = String.Empty Then` branch, within lines 118–122) | Direct assignment when `inPassword` is empty | `clsADUserServices.AddUser` | **CRITICAL** ([SEC-05](tech-debt.md#critical)). The account is created `.Enabled = True`. The literal value is not reproduced here or anywhere in this documentation set — see [Tech debt](tech-debt.md) |

## Security rules

### SEC-SVC-001 A local request is always approved

| Field | Value |
| --- | --- |
| **Statement** | Any request the ASP.NET runtime considers local to the server is unconditionally granted full access to the mutating surface |
| **Trigger** | `CheckAccess` evaluation, ahead of and independent of the allow-list |
| **Enforcement** | `If (HttpContext.Current.Request.IsLocal) Then response.HasAccess = True` — `clsADUserServices.asmx.vb` lines 762–765 |
| **Applies to** | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) and the eight gated operations |
| **Outcome** | `HasAccess = True`, `Local = True`. Restated as [BR-02](business-rules.md#br-02-a-local-request-is-always-approved). Any other process, site, or application co-hosted on the same server obtains the full directory-administration surface (SEC-11, MEDIUM) |
| **Source** | `clsADUserServices.asmx.vb:762-765` |

### SEC-SVC-002 An allow-listed source address is approved

| Field | Value |
| --- | --- |
| **Statement** | A request whose observed source address exactly (case-sensitive, untrimmed) matches a configured allow-list entry is granted access |
| **Trigger** | `CheckAccess` evaluation |
| **Enforcement** | `For Each ip In GetAllowedIPs()`, comparing each entry to `GetIncomingIp()`; on a match, sets `response.HasAccess = True` and `response.EnvironmentApi = True` — lines 767–772; comparison is raw string equality with no CIDR, parse, or trim (SEC-10, MEDIUM) |
| **Applies to** | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) and the eight gated operations |
| **Outcome** | `HasAccess = True`, `EnvironmentApi = True`. Restated as [BR-03](business-rules.md#br-03-an-allow-listed-source-address-is-approved) |
| **Source** | `clsADUserServices.asmx.vb:767-772` |

### SEC-SVC-003 The incoming-IP source prefers a spoofable header

| Field | Value |
| --- | --- |
| **Statement** | The address compared against the allow-list is taken from the `HTTP_X_FORWARDED_FOR` server variable, falling back to `REMOTE_ADDR` only when that header is null or empty, with no trusted-proxy validation |
| **Trigger** | Every `CheckAccess` evaluation |
| **Enforcement** | `GetIncomingIp` — lines 798–806 |
| **Applies to** | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) and the eight gated operations |
| **Outcome** | The chosen value feeds SEC-SVC-002; a caller able to set the header can present an arbitrary source address ([SEC-03](tech-debt.md#high), HIGH). Restated as [BR-04](business-rules.md#br-04-the-caller's-source-address-prefers-the-forwarded-for-header) |
| **Source** | `clsADUserServices.asmx.vb:798-806` |

### SEC-SVC-004 A denied request returns a normal response, never a fault

| Field | Value |
| --- | --- |
| **Statement** | A caller denied by `CheckAccess` receives a normal SOAP response carrying a failure sentinel, not a SOAP fault |
| **Trigger** | Denial under SEC-SVC-001/SEC-SVC-002 |
| **Enforcement** | `AccessDenied` — lines 781–789 |
| **Applies to** | the eight gated operations |
| **Outcome** | `ADServiceResponse{Response=Failure, Message="Access Denied"}`. Restated as [BR-05](business-rules.md#br-05-a-denied-request-is-reported-as-a-normal-response) |
| **Source** | `clsADUserServices.asmx.vb:781-789` |

### SEC-SVC-005 CheckAccess itself is published and unauthenticated

| Field | Value |
| --- | --- |
| **Statement** | The allow-list decision primitive is itself a published, unauthenticated operation that discloses the caller's observed remote address and whether it was accepted |
| **Trigger** | Any call to `CheckAccess` |
| **Enforcement** | No check precedes the body of `CheckAccess` — lines 752–776 |
| **Applies to** | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) |
| **Outcome** | `HasAccessResponse{HasAccess, Local, EnvironmentApi, RemoteAddress}` returned to any caller, providing an oracle for probing the allow-list ([SEC-04](tech-debt.md#high), MEDIUM) |
| **Source** | `clsADUserServices.asmx.vb:752-776` |

## Notes
- Mark inferred statements with `*` and unverifiable statements with `**`. Do not use bold inline confidence labels.

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with ** could not be confirmed; treat as unknown until verified.

## Related documentation
- [Service index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Operations and processes](operations-processes.md)
- [Business rules](business-rules.md)
- [Configuration](configuration.md)
- [Dependencies](dependencies.md)
- [Faults and error pipeline](faults.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

## Sources
- `src\ADUserServices\clsADUserServices.asmx.vb`
- `src\ADUserServices\Web.config`
- `pipelines\release.yml`
- `.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md`
- `.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-security-analysis.md`
